﻿//--------------------------------------------------------------------------
// 
//  Copyright (c) Chili Software.  All rights reserved. 
// 
//  File: ObjectPersister.cs
//
//  Description: Persists changes on persistent objects.
// 
//--------------------------------------------------------------------------

using System;
using System.Reflection;
using System.Collections.Generic;
using Chili.Opf3.Relations;
using Chili.Opf3.Storages;
using Chili.Opf3.Concurrency;
using Chili.Opf3.Query;
using Chili.Opf3.Mapping;

namespace Chili.Opf3
{
	/// <summary>
	/// This class is used by the <see cref="Opf3.ObjectContext">ObjectContext</see> to
	/// persist one single object.
	/// </summary>
	internal sealed class ObjectPersister
	{
		private ObjectContext _context;
		private ObjectDataAdapter _objectDataAdapter;

		/// <summary>
		/// Creates a new instance of the <see cref="ObjectPersister">ObjectPersister Class</see>.
		/// </summary>
		/// <param name="context">The <see cref="Opf3.ObjectContext">ObjectContext</see> associated with the 
		/// <see cref="ObjectPersister">ObjectPersister</see>.</param>
		public ObjectPersister(ObjectContext context)
		{
			_context = context;
			_objectDataAdapter = new ObjectDataAdapter(_context.Storage);
		}

		/// <summary>
		/// Returns the current persistent operation that is done on the persistent object.
		/// </summary>
		/// <param name="obj">The persistent object thats operation is returned.</param>
		internal PersistentOperations GetCurrentPersistentOperation(object obj)
		{
			ObjectInfo objectInfo = _context.GetObjectInfo(obj);

			if (objectInfo.MarkForDeletion)
				return PersistentOperations.Delete;

			if (objectInfo.ObjectState == ObjectStates.None || objectInfo.ObjectState == ObjectStates.Deleted)
				return PersistentOperations.Insert;
			else
				return PersistentOperations.Update;
		}

		#region PersistChanges

		/// <summary>
		/// Persists the persistent object given.
		/// </summary>
		/// <param name="obj">Persistent object that is persisted.</param>
		/// <param name="persistDepth">Specifies the depth of the current persist operation.</param>
		public void PersistChanges<T>(T obj, PersistDepths persistDepth)
		{
			ObjectInfo objectInfo = _context.GetObjectInfo(obj);

			ITransactionStorage storage = _context.Storage as ITransactionStorage;
			// Check if the storage is a transaction storage and if a transaction is active.
			if (storage != null && storage.IsTransactionActive)
				storage.ActiveTransaction.AddObjectInfoInternal(objectInfo);

			// Get the mapping of the type.
			TypeMapping typeMapping = TypeMapping.GetTypeMapping(obj);

			if (objectInfo.MarkForDeletion)
			{
				// Check if the members have an identifier specified.
				if (typeMapping.Members.GetIdentifierMemberMappings().Count == 0)
				{
					throw new InvalidOperationException(string.Format("The persistent type '{0}' does not have a member that is marked as identifier. " +
						"Use the FieldAttribute's Identifier property to specify at least one member as identifier.", obj.GetType().FullName));
				}

				// Check if the type has the right to be deleted.
				if ((typeMapping.PersistentAttribute.Rights & PersistentRights.Delete) == 0)
					throw new InvalidOperationException("The persistent object type has no right to be deleted.");

				// If the object is already deleted or hasn't been loaded, don't try to delete.
				if (objectInfo.ObjectState == ObjectStates.Deleted || objectInfo.ObjectState == ObjectStates.None)
				{
					Tracing.Current.TraceWarning("Tried to delete a persistent object that hasn't been loaded, saved or inserted.");
					return;
				}

				// Delete the object.
				Delete<T>(obj, objectInfo, persistDepth);

				// remove the object from the identity map after having it deleted.
				if (_context.IdentityMap.Enabled)
					_context.IdentityMap.Remove(obj);

				return;
			}

			// Skip if we have a persistent that is only to persist partial and has no members to persist.
			IPartialPersist partial = obj as IPartialPersist;
			if (partial != null && !partial.HasMembersToPersist)
				return;

			// If the object has been loaded or deleted, insert it. Set also the new state 
			// of the object.
			if (objectInfo.ObjectState == ObjectStates.None || objectInfo.ObjectState == ObjectStates.Deleted)
			{
				// Check if the type has the right to be inserted.
				if ((typeMapping.PersistentAttribute.Rights & PersistentRights.Insert) == 0)
					throw new InvalidOperationException("The persistent object type has no right to be inserted.");

				Insert<T>(obj, objectInfo, persistDepth);
			}
			else
			{
				// Check if the members have an identifier specified.
				if (typeMapping.Members.GetIdentifierMemberMappings().Count == 0)
				{
					throw new InvalidOperationException(string.Format("The persistent type '{0}' does not have a member that is marked as identifier. " +
						"Use the FieldAttribute's Identifier property to specify at least one member as identifier.", obj.GetType().FullName));
				}

				// Check if the new checksum for the object equals with the old one.
				if (objectInfo.Checksum == null ||
					ChecksumBuilder.EqualsChecksum(objectInfo.Checksum, ChecksumBuilder.CalculatePersistentChecksum(obj)))
				{
					return;
				}
				// Check if the type has the right to be inserted.
				if ((typeMapping.PersistentAttribute.Rights & PersistentRights.Update) == 0)
					throw new InvalidOperationException("The persistent object type has no right to be updated.");

				Update<T>(obj, objectInfo, persistDepth);
			}

			// add the object to the identity map if the identity map is enabled.
			if (_context.IdentityMap.Enabled)
				_context.IdentityMap.Add(obj);

			// Add the object to the cache if required.
			if (_context.CacheManager != null)
				_context.CacheManager.AddPersistentObject(obj);
		}

		#endregion

		#region Delete

		/// <summary>
		/// Deletes the object in the storage.
		/// </summary>
		/// <param name="obj">Object that is deleted.</param>
		/// <param name="objectInfo">Object that is used to change the state of the object.</param>
		/// <param name="persistDepth">Specifies the depth of the current persist operation.</param>
		private void Delete<T>(T obj, ObjectInfo objectInfo, PersistDepths persistDepth)
		{
			IConcurrencyManager concurrencyManager = _context.ConcurrencyManager;
			IObjectNotification objectNotification = obj as IObjectNotification;

			// Notify the concurrency manager that something happened.
			if (concurrencyManager != null)
				concurrencyManager.OnBeforeDelete(obj, new ConcurrencyManagerEventArgs(objectInfo, _context));

			// Compile delete query.
			IQuery query = null;
			// Get the query from the persistent queries provider.
			IPersistentQueriesProvider provider = null;
			if (_context.PersistentQueriesProviders.TryGetValue(typeof(T), out provider))
				query = provider.GetDeleteQuery(_context.Storage, obj);

			if (query == null)
				query = new PersistentObjectExpression(QueryTypes.Delete, obj);

			// Fire the ObjectContext event.
			ObjectPersistingEventArgs objectContextEventArgs = new ObjectPersistingEventArgs(query, obj, objectInfo);
			OnDeleting(objectContextEventArgs);
			query = objectContextEventArgs.Query;

			ObjectNotificationEventArgs onArgs = null;
			if (objectNotification != null)
			{
				onArgs = new ObjectNotificationEventArgs(query, objectInfo, _context, PersistentOperations.Delete, persistDepth);
				objectNotification.OnBeforeDelete(onArgs);
				// Set the query that returns as the query that is executed.
				query = onArgs.Query;
			}

			try
			{
				// Execute the command on the storage.
				_context.Storage.ExecuteCommand(query);
				// Set the object deleted.
				objectInfo.ObjectState = ObjectStates.Deleted;
				// The object has been deleted. It is no longer marked as for deletion.
				objectInfo.MarkForDeletion = false;
			}
			catch
			{
				// Fire the event.
				OnDeleted(new ObjectPersistedEventArgs(obj, objectInfo, false));
				throw;
			}

			// Fire the event.
			OnDeleted(new ObjectPersistedEventArgs(obj, objectInfo, true));
			if (objectNotification != null)
				objectNotification.OnAfterDelete(onArgs);
		}

		#endregion

		#region Insert

		/// <summary>
		/// Inserts the object in the storage.
		/// </summary>
		/// <param name="obj">Object that is inserted.</param>
		/// <param name="objectInfo">Object that is used to change the state of the object.</param>
		/// <param name="persistDepth">Specifies the depth of the current persist operation.</param>
		private void Insert<T>(T obj, ObjectInfo objectInfo, PersistDepths persistDepth)
		{
			IObjectNotification objectNotification = obj as IObjectNotification;
			IConcurrencyManager concurrencyManager = _context.ConcurrencyManager;
			ConcurrencyManagerEventArgs e = new ConcurrencyManagerEventArgs(objectInfo, _context);

			// Notify the concurrency manager that something happened.
			if (concurrencyManager != null)
				concurrencyManager.OnBeforeInsert(obj, e);

			// Compile insert query.
			IQuery query = null;
			// Get the query from the persistent queries provider.
			IPersistentQueriesProvider provider = null;
			if (_context.PersistentQueriesProviders.TryGetValue(typeof(T), out provider))
				query = provider.GetInsertQuery(_context.Storage, obj);

			// Create the query.
			if (query == null)
				query = new PersistentObjectExpression(QueryTypes.Insert, obj);

			// Fire the ObjectContext event.
			ObjectPersistingEventArgs objectContextEventArgs = new ObjectPersistingEventArgs(query, obj, objectInfo);
			OnInserting(objectContextEventArgs);
			query = objectContextEventArgs.Query;

			ObjectNotificationEventArgs onArgs = null;
			if (objectNotification != null)
			{
				onArgs = new ObjectNotificationEventArgs(query, objectInfo, _context, PersistentOperations.Insert, persistDepth);
				objectNotification.OnBeforeInsert(onArgs);
				// Set the query that returns as the query that is executed.
				query = onArgs.Query;
			}

			try
			{
				// Execute the command on the storage.
				object returnValue = _context.Storage.ExecuteCommand(query);
				if (returnValue != null)
					_objectDataAdapter.FillAutoNumber(obj, returnValue);

				// Create the new checksum and set it as checksum in the objectinfo.
				objectInfo.Checksum = ChecksumBuilder.CalculatePersistentChecksum(obj);
				objectInfo.ObjectState = ObjectStates.Inserted;
			}
			catch
			{
				// Fire the event.
				OnInserted(new ObjectPersistedEventArgs(obj, objectInfo, false));
				throw;
			}

			// Fire the event.
			OnInserted(new ObjectPersistedEventArgs(obj, objectInfo, true));
			// Notify the concurrency manager that something happened.
			if (concurrencyManager != null)
				concurrencyManager.OnAfterInsert(obj, e);
			if (objectNotification != null)
				objectNotification.OnAfterInsert(onArgs);
		}

		#endregion

		#region Update

		/// <summary>
		/// Updates the object in the storage.
		/// </summary>
		/// <param name="obj">Object that is updated.</param>
		/// <param name="objectInfo">Object that is used to change the state of the object.</param>
		/// <param name="persistDepth">Specifies the depth of the current persist operation.</param>
		private void Update<T>(T obj, ObjectInfo objectInfo, PersistDepths persistDepth)
		{
			IObjectNotification objectNotification = obj as IObjectNotification;
			IConcurrencyManager concurrencyManager = _context.ConcurrencyManager;

			ConcurrencyManagerEventArgs e = new ConcurrencyManagerEventArgs(objectInfo, _context);
			// Notify the concurrency manager that something happened.
			if (concurrencyManager != null)
				concurrencyManager.OnBeforeUpdate(obj, e);

			// Compile update query.
			IQuery query = null;
			// Get the query from the persistent queries provider.
			IPersistentQueriesProvider provider = null;
			if (_context.PersistentQueriesProviders.TryGetValue(typeof(T), out provider))
				query = provider.GetUpdateQuery(_context.Storage, obj);

			// Create the query.
			if (query == null)
				query = new PersistentObjectExpression(QueryTypes.Update, obj);

			// Fire the ObjectContext event.
			ObjectPersistingEventArgs objectContextEventArgs = new ObjectPersistingEventArgs(query, obj, objectInfo);
			OnUpdating(objectContextEventArgs);
			query = objectContextEventArgs.Query;

			ObjectNotificationEventArgs onArgs = null;
			if (objectNotification != null)
			{
				onArgs = new ObjectNotificationEventArgs(query, objectInfo, _context, PersistentOperations.Update, persistDepth);
				objectNotification.OnBeforeUpdate(onArgs);
				// Set the query that returns as the query that is executed.
				query = onArgs.Query;
			}

			try
			{
				// Execute the command on the storage.
				_context.Storage.ExecuteCommand(query);

				// Set the new checksum as the checksum of the objectInfo. We have to calculate
				// the checksum also here, because the object could have been altered (concurrency manager etc.).
				objectInfo.Checksum = ChecksumBuilder.CalculatePersistentChecksum(obj);
				objectInfo.ObjectState = ObjectStates.Updated;
			}
			catch
			{
				// Fire the event.
				OnUpdated(new ObjectPersistedEventArgs(obj, objectInfo, false));
				throw;
			}

			// Fire the event.
			OnUpdated(new ObjectPersistedEventArgs(obj, objectInfo, true));
			// Notify the concurrency manager that something happened.
			if (concurrencyManager != null)
				concurrencyManager.OnAfterUpdate<T>(obj, e);
			if (objectNotification != null)
				objectNotification.OnAfterUpdate(onArgs);
		}

		#endregion

		#region Events

		/// <summary>
		/// Event fired before an object is updated.
		/// </summary>
		public event EventHandler<ObjectPersistingEventArgs> Updating;

		/// <summary>
		/// Event fired after an object is updated.
		/// </summary>
		public event EventHandler<ObjectPersistedEventArgs> Updated;

		/// <summary>
		/// Event fired before an object is inserted.
		/// </summary>
		public event EventHandler<ObjectPersistingEventArgs> Inserting;

		/// <summary>
		/// Event fired after an object is inserted.
		/// </summary>
		public event EventHandler<ObjectPersistedEventArgs> Inserted;

		/// <summary>
		/// Event fired before an object is deleted.
		/// </summary>
		public event EventHandler<ObjectPersistingEventArgs> Deleting;

		/// <summary>
		/// Event fired after an object is deleted.
		/// </summary>
		public event EventHandler<ObjectPersistedEventArgs> Deleted;

		/// <summary>
		/// Invoked before an object is inserted.
		/// </summary>
		/// <param name="e">EventArgs of the routine.</param>
		private void OnInserting(ObjectPersistingEventArgs e)
		{
			if (Inserting != null)
				Inserting(this, e);
		}

		/// <summary>
		/// Invoked after an object is inserted.
		/// </summary>
		/// <param name="e">EventArgs of the routine.</param>
		private void OnInserted(ObjectPersistedEventArgs e)
		{
			if (Inserted != null)
				Inserted(this, e);
		}

		/// <summary>
		/// Invoked before an object is updated.
		/// </summary>
		/// <param name="e">EventArgs of the routine.</param>
		private void OnUpdating(ObjectPersistingEventArgs e)
		{
			if (Updating != null)
				Updating(this, e);
		}

		/// <summary>
		/// Invoked after an object is updated.
		/// </summary>
		/// <param name="e">EventArgs of the routine.</param>
		private void OnUpdated(ObjectPersistedEventArgs e)
		{
			if (Updated != null)
				Updated(this, e);
		}

		/// <summary>
		/// Invoked before an object is deleted.
		/// </summary>
		/// <param name="e">EventArgs of the routine.</param>
		private void OnDeleting(ObjectPersistingEventArgs e)
		{
			if (Deleting != null)
				Deleting(this, e);
		}

		/// <summary>
		/// Invoked after an object is deleted.
		/// </summary>
		/// <param name="e">EventArgs of the routine.</param>
		private void OnDeleted(ObjectPersistedEventArgs e)
		{
			if (Deleted != null)
				Deleted(this, e);
		}

		#endregion
	}
}
